﻿namespace MakeVSProject4FFmpeg
{
    #region using directives
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    #endregion

    class MakeFileLoader : IProjInformation
    {
        private List<CompilationUnit> depFiles;

        public MakeFileLoader()
        {
            this.depFiles = new List<CompilationUnit>();
        }

        public void ParseFile(string fileName)
        {
            using (FileStream fs = File.OpenRead(fileName))
            {
                using (TextReader reader = new StreamReader(fs))
                {
                    this.Parse(reader);
                }
            }
        }

        public void Parse(TextReader textReader)
        {
            for (; ; )
            {
                string line = textReader.ReadLine();
                if (line == null)
                {
                    break;
                }

                this.ParseLine(line.Trim());
            }

            this.Consolidate();
        }

        public void ParseDependencies(string distributionFolder)
        {
            var compUnits = this.depFiles.Where(compUnit => compUnit.Type == FileType.CFile || compUnit.Type == FileType.AsmFile).ToList();
            foreach (CompilationUnit compilationUnit in compUnits)
            {
                if (!distributionFolder.EndsWith("/") && !distributionFolder.EndsWith("\\"))
                {
                    distributionFolder += '\\';
                }

                this.CheckForDependencies(distributionFolder, compilationUnit);
            }
        }

        private static List<string> x264CFiles = new List<string>()
                                                    {
                                                        @"common\bitstream.c",
                                                        @"common\cabac.c",
                                                        @"common\common.c",
                                                        @"common\cpu.c",
                                                        @"common\dct.c",
                                                        @"common\deblock.c",
                                                        @"common\frame.c",
                                                        @"common\macroblock.c",
                                                        @"common\mc.c",
                                                        @"common\mvpred.c",
                                                        @"common\osdep.c",
                                                        @"common\pixel.c",
                                                        @"common\predict.c",
                                                        @"common\quant.c",
                                                        @"common\rectangle.c",
                                                        @"common\set.c",
                                                        @"common\threadpool.c",
                                                        @"common\vlc.c",
                                                        @"common\win32thread.c",

                                                        @"encoder\analyse.c",
                                                        @"encoder\cabac.c",
                                                        @"encoder\cavlc.c",
                                                        @"encoder\encoder.c",
                                                        @"encoder\lookahead.c",
                                                        @"encoder\macroblock.c",
                                                        @"encoder\me.c",
                                                        @"encoder\ratecontrol.c",
                                                        @"-|encoder\rdo.c",
                                                        @"encoder\set.c",
                                                        @"-|encoder\slicetype.c",

                                                        @"common\x86\mc-c.c",
                                                        @"common\x86\predict-c.c"
                                                    };

        private static List<string> x264AsmFiles = new List<string>()
                                                       {
                                                           @"common\x86\bitstream-a.asm",
                                                           @"common\x86\cabac-a.asm",
                                                           @"common\x86\const-a.asm",
                                                           @"common\x86\cpu-a.asm",
                                                           @"-x64|common\x86\dct-32.asm",
                                                           @"-x86|common\x86\dct-64.asm",
                                                           @"common\x86\dct-a.asm",
                                                           @"common\x86\deblock-a.asm",
                                                           @"common\x86\mc-a.asm",
                                                           @"common\x86\mc-a2.asm",
                                                           @"-x64|common\x86\pixel-32.asm",
                                                           @"common\x86\pixel-a.asm",
                                                           @"common\x86\predict-a.asm",
                                                           @"common\x86\quant-a.asm",
                                                           @"common\x86\sad-a.asm",
                                                           @"-|common\x86\sad16-a.asm",
                                                           @"-x86|common\x86\trellis-64.asm",
                                                           @"-|common\x86\x86inc.asm",
                                                           @"-|common\x86\x86util.asm"
                                                       };

        private static List<string> x264HeaderFiles = new List<string>()
                                                          {
                                                              @"common\x86\dct.h",
                                                              @"common\x86\mc.h",
                                                              @"common\x86\pixel.h",
                                                              @"common\x86\predict.h",
                                                              @"common\x86\quant.h",
                                                              @"common\x86\util.h",
                                                              @"common\bitstream.h",
                                                              @"common\cabac.h",
                                                              @"common\common.h",
                                                              @"common\cpu.h",
                                                              @"common\dct.h",
                                                              @"common\display.h",
                                                              @"common\frame.h",
                                                              @"common\macroblock.h",
                                                              @"common\mc.h",
                                                              @"common\osdep.h",
                                                              @"common\pixel.h",
                                                              @"common\predict.h",
                                                              @"common\quant.h",
                                                              @"common\rectangle.h",
                                                              @"common\set.h",
                                                              @"common\threadpool.h",
                                                              @"common\visualize.h",
                                                              @"common\win32thread.h",
                                                              @"common\x264.h",
                                                              @"encoder\analyse.h",
                                                              @"encoder\macroblock.h",
                                                              @"encoder\me.h",
                                                              @"encoder\ratecontrol.h",
                                                              @"encoder\set.h",
                                                              @"config.h",
                                                              @"x264.h",
                                                              @"x264_config.h"
                                                          };

        private static List<string> zlibCFiles = new List<string>()
            {
                "adler32.c", "compress.c", "crc32.c", "deflate.c", "gzclose.c", "gzlib.c",
                "gzread.c", "gzwrite.c", "infback.c", "inffast.c", "inflate.c", "inftrees.c",
                "trees.c", "uncompr.c", "zutil.c"
            };

        private static List<string> zlibHeaders = new List<string>()
            {
                "crc32.h","deflate.h","gzguts.h","inffast.h","inffixed.h","inflate.h",
                "inftrees.h","trees.h","zconf.h","zlib.h","zutil.h"
            };

        public void AddZlib(string zlibFolder)
        {
            CompilationUnit cu;
            foreach (string zlibFile in zlibCFiles)
            {
                string fileName = CanonicalizeFileName(Path.Combine(zlibFolder, NormalizeFileName(zlibFile)));
                cu = new CompilationUnit(
                    fileName,
                    FileType.CFile,
                    IsUnconditionallyExcludedFromBuild(zlibFile),
                    MakeObjectFilePrefix(NormalizeFileName(zlibFile), "zlib"),
                    @"$(ProjectDir)\external\zlib;%(AdditionalIncludeDirectories)",
                    null)
                {
                    MustTweakObjFileName = true
                };

                this.depFiles.Add(cu);
            }

            // add the lcldec.c & lclenc.c file from ffmpeg
            cu = this.depFiles.FirstOrDefault((x) => string.CompareOrdinal(x.GetProjectRelativeFilenameWithForwardSlash(), @"libavcodec/lcldec.c") == 0);
            if (cu == null)
            {
                cu = new CompilationUnit(
                    @"libavcodec/lcldec.c",
                    FileType.CFile,
                    false,
                    null,
                    @"%(AdditionalIncludeDirectories);$(ProjectDir)\external\zlib",
                    null);
                this.depFiles.Add(cu);
            }
            else
            {
                cu.AdditionalIncludeDirectories = @"%(AdditionalIncludeDirectories);$(ProjectDir)\external\zlib";
            }

            cu = this.depFiles.FirstOrDefault((x) => string.CompareOrdinal(x.GetProjectRelativeFilenameWithForwardSlash(), @"libavcodec/lclenc.c") == 0);
            if (cu == null)
            {
                cu = new CompilationUnit(
                    @"libavcodec/lclenc.c",
                    FileType.CFile,
                    false,
                    null,
                    @"%(AdditionalIncludeDirectories);$(ProjectDir)\external\zlib",
                    null);
                this.depFiles.Add(cu);
            }
            else
            {
                cu.AdditionalIncludeDirectories = @"%(AdditionalIncludeDirectories);$(ProjectDir)\external\zlib";
            }

            foreach (string s in new[]
                                 {
                                     "libavcodec/cscd.c", 
                                     "libavcodec/svq3.c", 
                                     "libavcodec/tiff.c", 
                                     "libavcodec/tiffenc.c", 
                                     "libavformat/id3v2.c", 
                                     "libavformat/matroskadec.c", 
                                     "libavformat/mov.c", 
                                     "libavformat/rtmpproto.c", 
                                     "libavformat/swfdec.c", 
                                     "libavformat/swfenc.c",
                                     "libavformat/http.c"
                                 })
            {
                cu = this.depFiles.FirstOrDefault((x) => string.CompareOrdinal(x.GetProjectRelativeFilenameWithForwardSlash(), s) == 0);
                if (cu != null)
                {
                    cu.AdditionalIncludeDirectories = @"%(AdditionalIncludeDirectories);$(ProjectDir)\external\zlib";
                }
            }

            foreach (string zlibFile in zlibHeaders)
            {
                string fileName = CanonicalizeFileName(Path.Combine(zlibFolder, NormalizeFileName(zlibFile)));
                cu = new CompilationUnit(fileName, FileType.HeaderFile);
                this.depFiles.Add(cu);
            }
        }

        public void AddX264(string x264Folder)
        {
            CompilationUnit cu;
            foreach (string x264File in x264CFiles)
            {
                string fileName = CanonicalizeFileName(Path.Combine(x264Folder, NormalizeFileName(x264File)));
                cu = new CompilationUnit(
                    fileName,
                    FileType.CFile,
                    IsUnconditionallyExcludedFromBuild(x264File),
                    MakeObjectFilePrefix(NormalizeFileName(x264File), "x264"),
                    @"$(ProjectDir)\external\x264;%(AdditionalIncludeDirectories)",
                    @"HAVE_THREAD=1;SYS_WINDOWS=1;__ICL;HAVE_WIN32THREAD=1;HAVE_GPL;BROKEN_STACK_ALIGNMENT=1;HAVE_INTERLACED=1;HAVE_MMX=1;%(PreprocessorDefinitions)")
                                         {
                                             MustTweakObjFileName = true
                                         };
                this.depFiles.Add(cu);
            }

            // add the libx264.c file from ffmpeg
            cu = new CompilationUnit(
                                 @"libavcodec/libx264.c",
                                FileType.CFile,
                                false,
                                null,
                                @"%(AdditionalIncludeDirectories);$(ProjectDir)\external\x264",
                                null);
            this.depFiles.Add(cu);

            foreach (string x264File in x264AsmFiles)
            {
                bool isUnconditionallyExcludedFromBuild = IsUnconditionallyExcludedFromBuild(x264File);
                string fileName = CanonicalizeFileName(Path.Combine(x264Folder, NormalizeFileName(x264File)));
                cu = new CompilationUnit(
                    fileName,
                    FileType.AsmFile,
                    isUnconditionallyExcludedFromBuild);

                if (isUnconditionallyExcludedFromBuild == false)
                {
                    if (x264File.StartsWith("-x86|"))
                    {
                        cu.AddExcludeFromCompilationWithCondition("Win32");
                    }
                    else if (x264File.StartsWith("-x64|"))
                    {
                        cu.AddExcludeFromCompilationWithCondition("x64");
                    }
                }

                cu.AddNodeConditionValue(
                                    "Defines",
                                    "'$(Configuration)|$(Platform)'=='Debug|Win32'",
                                    "ARCH_X86_64=0;ARCH_X86_32=1;HIGH_BIT_DEPTH=0;BIT_DEPTH=8;WIN32=1;PREFIX;HAVE_ALIGNED_STACK=0");
                cu.AddNodeConditionValue(
                                    "Defines",
                                    "'$(Configuration)|$(Platform)'=='Release|Win32'",
                                    "ARCH_X86_64=0;ARCH_X86_32=1;HIGH_BIT_DEPTH=0;BIT_DEPTH=8;WIN32=1;PREFIX;HAVE_ALIGNED_STACK=0");
                cu.AddNodeConditionValue(
                                    "Defines",
                                    "'$(Configuration)|$(Platform)'=='Debug|x64'",
                                    "ARCH_X86_64=1;ARCH_X86_32=0;HIGH_BIT_DEPTH=0;BIT_DEPTH=8;WIN64=1;HAVE_ALIGNED_STACK=1");
                cu.AddNodeConditionValue(
                                    "Defines",
                                    "'$(Configuration)|$(Platform)'=='Release|x64'",
                                    "ARCH_X86_64=1;ARCH_X86_32=0;HIGH_BIT_DEPTH=0;BIT_DEPTH=8;WIN64=1;HAVE_ALIGNED_STACK=1");

                this.depFiles.Add(cu);
            }

            foreach (string x264File in x264HeaderFiles)
            {
                string fileName = CanonicalizeFileName(Path.Combine(x264Folder, NormalizeFileName(x264File)));
                cu = new CompilationUnit(fileName, FileType.HeaderFile);
                this.depFiles.Add(cu);
            }
        }

        /// <summary>
        /// Normalizes the name of the file if it starts with '-' it means that it is to be excluded
        /// from the build. This function will in this case skip the '-'.
        /// </summary>
        /// <param name="s">The string to normalize.</param>
        /// <returns>The normalized string (filename with processing instructions removed).</returns>
        private static string NormalizeFileName(string s)
        {
            int index = s.IndexOf('|');
            if (index > 0)
            {
                return s.Substring(1 + index);
            }

            return s;
        }

        private static bool IsUnconditionallyExcludedFromBuild(string x264File)
        {
            if (x264File.StartsWith("-|"))
            {
                return true;
            }

            return false;
        }

        private static string MakeObjectFilePrefix(string filename, string prefix)
        {
            int index = filename.IndexOf('\\');
            if (index > 1)
            {
                string s = filename.Substring(0, index);
                return prefix + "_" + s;
            }

            return prefix;
        }

        private void CheckForDependencies(string distributionFolder, CompilationUnit compilationUnit)
        {
            string depFile = Path.Combine(distributionFolder, Path.Combine(compilationUnit.PathWithoutFileName, compilationUnit.FileNameWithoutExtension + ".d"));
            if (File.Exists(depFile))
            {
                this.ParseDepFile(distributionFolder, depFile);
            }
        }

        private void ParseDepFile(string distributionFolder, string depFile)
        {
            List<string> depList = ReadAllLinesOfFile(depFile);
            depList = GetAllDependencies(depList);
            foreach (string s in depList)
            {
                this.ProcessDependency(distributionFolder, s);
            }
        }

        private List<string> GetAllDependencies(IList<string> lines)
        {
            List<string> listOfFiles = new List<string>(lines.Count);
            for (int i = 0; i < lines.Count; i++)
            {
                string line = lines[i];
                for (; line.EndsWith(@"\") && i + 1 < lines.Count; i++)
                {
                    line = line.Substring(0, line.Length - 1) + lines[i + 1];
                }

                string[] split = line.Split(':');

                string[] files = split[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

                listOfFiles.AddRange(files.Select(file => file.Trim()));
            }

            return listOfFiles;
        }

        private List<string> ReadAllLinesOfFile(string dFileName)
        {
            List<string> lines = new List<string>();
            using (StreamReader r = new StreamReader(dFileName))
            {
                string line;
                while ((line = r.ReadLine()) != null)
                {
                    lines.Add(line);
                }
            }

            return lines;
        }

        private void ProcessDependency(string distributionFolder, string line)
        {
            string hdrFileName;
            if (line.StartsWith("/"))
            {
                hdrFileName = this.TryFindAbsoluteCygwinPath(distributionFolder, line);
            }
            else
            {
                hdrFileName = this.TryFindRelativePath(distributionFolder, line);
            }

            Debug.Assert(!string.IsNullOrEmpty(hdrFileName));
            if (!string.IsNullOrEmpty(hdrFileName))
            {
                System.Uri uri1 = new Uri(hdrFileName);
                System.Uri uri2 = new Uri(distributionFolder);
                Uri relativeUri = uri2.MakeRelativeUri(uri1);
                string fileName = CanonicalizeFileName(relativeUri.ToString());
                this.AddHeaderFileIfNotPresent(fileName);
            }
        }

        private string TryFindRelativePath(string distributionFolder, string fileName)
        {
            string fileNameToTest = Path.Combine(distributionFolder, fileName);
            if (File.Exists(fileNameToTest))
            {
                return fileNameToTest;
            }

            return null;
        }

        private string TryFindAbsoluteCygwinPath(string distributionFolder, string fileName)
        {
            for (int indexStart = 0; ; )
            {
                indexStart = fileName.IndexOf('/', indexStart);
                if (indexStart < 0)
                {
                    return null;
                }

                indexStart++;
                string fileNameToTest = Path.Combine(distributionFolder, fileName.Substring(indexStart));
                if (File.Exists(fileNameToTest))
                {
                    return fileNameToTest;
                }
            }
        }

        private void Consolidate()
        {
            // second, look for compilation units (which produce object-code, i.e. c- and asm-files)
            this.depFiles.Sort(
                (a, b) =>
                {
                    bool aIsProducingObj = IsProducingObjectCode(a.Type);
                    bool bIsProducingObj = IsProducingObjectCode(b.Type);
                    if (aIsProducingObj == true && bIsProducingObj == false)
                    {
                        return -1;
                    }
                    else if (aIsProducingObj == false && bIsProducingObj == true)
                    {
                        return 1;
                    }
                    else
                    {
                        return string.CompareOrdinal(a.FileNameWithoutExtension, b.FileNameWithoutExtension);
                    }
                });

            for (int i = 0; i < this.depFiles.Count - 1; i++)
            {
                if (IsProducingObjectCode(this.depFiles[i].Type) == false)
                {
                    break;
                }

                if (string.CompareOrdinal(this.depFiles[i].FileNameWithoutExtension, "file_open") == 0 &&
                    string.CompareOrdinal(this.depFiles[i].FileNameWithoutExtension,this.depFiles[i+1].FileNameWithoutExtension)==0)
                {
                    this.depFiles[i + 1].ExcludeFromBuild = true;
                }

                if (string.CompareOrdinal(
                    this.depFiles[i].FileNameWithoutExtension, this.depFiles[i + 1].FileNameWithoutExtension) == 0)
                {
                    // we can only (easily) change the obj-filename for C-files, so
                    // if one of the items is an asm-file, then choose the C-file
                    if (this.depFiles[i].Type == FileType.AsmFile && this.depFiles[i + 1].Type == FileType.CFile)
                    {
                        this.depFiles[i + 1].MustTweakObjFileName = true;
                        Debug.Assert(
                            this.depFiles[i].MustTweakObjFileName == false,
                            "The object-filename of Asm-Files cannot be changed.");
                    }
                    else if (this.depFiles[i].Type == FileType.CFile && this.depFiles[i + 1].Type == FileType.AsmFile)
                    {
                        this.depFiles[i].MustTweakObjFileName = true;
                        Debug.Assert(
                            this.depFiles[i + 1].MustTweakObjFileName == false,
                            "The object-filename of Asm-Files cannot be changed.");
                    }
                    else if (this.depFiles[i].Type == FileType.CFile && this.depFiles[i + 1].Type == FileType.CFile)
                    {
                        this.depFiles[i + 1].MustTweakObjFileName = true;
                    }
                    else
                    {
                        throw new InvalidOperationException("The object-filename of Asm-Files cannot be changed.");
                    }
                }
            }
        }

        static private bool IsProducingObjectCode(FileType type)
        {
            switch (type)
            {
                case FileType.CFile:
                case FileType.AsmFile:
                    return true;
                case FileType.HeaderFile:
                    return false;
            }

            throw new ArgumentException("Unknown filetype.");
        }

        private void ParseLine(string line)
        {
            if (line.StartsWith("printf \"CC"))
            {
                this.AddCFile(ExtractFilename(line));
            }
            else if (line.StartsWith("printf \"YASM"))
            {
                this.AddAsmFile(ExtractFilename(line));
            }
        }

        private static string ExtractFilename(string line)
        {
            int indexPipe = line.IndexOf('|');
            if (indexPipe > 0)
            {
                line = line.Substring(0, indexPipe);
            }

            string filename = line.Substring(line.LastIndexOf(" ", StringComparison.Ordinal) + 1);
            filename = filename.Replace('\\', '/').Trim();
            for (; ; )
            {
                int index = filename.IndexOf("/..", System.StringComparison.Ordinal);
                if (index < 0)
                {
                    break;
                }

                int index2 = filename.IndexOf('/');
                if (index2 >= index)
                {
                    index2 = 0;
                }

                if (index2 < index)
                {
                    filename = filename.Substring(0, index2) + filename.Substring(index + 2);
                }
            }

            return filename;
        }

        private void AddCFile(string fileName)
        {
            string fName = CanonicalizeFileName(fileName);

            if (this.FilterCFile(fileName))
            {
                CompilationUnit cu = new CompilationUnit(fName, FileType.CFile);
                this.depFiles.Add(cu);
            }
        }

        private void AddAsmFile(string fileName)
        {
            CompilationUnit cu = new CompilationUnit(fileName, FileType.AsmFile);
            this.depFiles.Add(cu);
        }

        static bool CompareFileNamesForEquality(string a, string b)
        {
            int offsetA = 0;
            if (a.StartsWith("./") || a.StartsWith(".\\"))
            {
                offsetA = 2;
            }

            int offsetB = 0;
            if (b.StartsWith("./") || b.StartsWith(".\\"))
            {
                offsetB = 2;
            }

            int r = string.CompareOrdinal(a, offsetA, b, offsetB, a.Length - offsetA);
            return r == 0 ? true : false;
        }

        private void AddHeaderFileIfNotPresent(string fileName)
        {
            bool exists = this.depFiles.Exists(e => CompareFileNamesForEquality(fileName, e.GetProjectRelativeFilenameWithForwardSlash()));
            if (!exists)
            {
                if (fileName.EndsWith(".c") || fileName.EndsWith(".C"))
                {
                    CompilationUnit cu = new CompilationUnit(fileName, FileType.CFile, true);
                    this.depFiles.Add(cu);
                }
                else
                {

                    CompilationUnit cu = new CompilationUnit(fileName, FileType.HeaderFile);
                    this.depFiles.Add(cu);
                }
            }
        }

        protected bool FilterCFile(string fileName)
        {
            if (fileName.ToLowerInvariant().EndsWith("ffprobe.c"))
            {
                return false;
            }

            return true;
        }

        private static string CanonicalizeFileName(string fileName)
        {
            string fName = fileName.Replace('\\', '/').Trim();
            if (!fName.Contains('/'))
            {
                fName = "./" + fName;
            }

            return fName;
        }

        public IEnumerable<CompilationUnit> GetFiles(FileType type)
        {
            return this.depFiles.Where(compUnit => compUnit.Type == type);
        }

        public IEnumerable<string> GetFilterFolders()
        {
            var filterFolders = this.depFiles
                                    .Select(compUnit => compUnit.PathWithoutFileName)
                                    .Where(f => !string.IsNullOrEmpty(f))
                                    .Distinct();
            return filterFolders;
        }
    }
}
